#include "autocompleter.h"
#include "Preferences/preferences.h"

#include <QKeyEvent>
#include <QScrollBar>
#include <QApplication>
#include <QDesktopWidget>


const int AutoCompletion::MAX_WIDTH = 200;
const int AutoCompletion::MAX_HEIGHT = 200;


AutoCompletion::AutoCompletion(TextEditor *newEditor) : QListWidget(newEditor)
{
	editor = newEditor;
	shortcutPressed = false;

	setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint
			     | Qt::Dialog);

	horizontalScrollBar()->setSingleStep(20);
	caseSensitivity = Qt::CaseInsensitive;
}

/**
  * Update the list of completions from the words in the document.
  */
void AutoCompletion::updateCompletions()
{
	QString text = editor->toPlainText();

	text.truncate(editor->toPlainText().lastIndexOf(QRegExp("\\s+")));
	text = text.simplified();

	clear();

	if (text.length() > 0)
	{
		QStringList words = text.split(QRegExp("\\s+"));

		words.removeDuplicates();

		// Remove the word that is typed already so that it doesn't show up as a completion.
		words.removeOne(editor->getWordToComplete());

		addItems(words);
	}
}

/**
  * Process a key press on the attached editor. This may result in completion events or the event
  * being relayed back to the editor for handling.
  *
  * This funciton is officially disgusting as there are many conditions. DEAL WITH IT.
  */
void AutoCompletion::keyPressEvent(QKeyEvent *event)
{
	shortcutPressed = ((event->modifiers() & Qt::ControlModifier) && event->key() == Qt::Key_E);

	/*
	  * Check if the event was a modifier event (alt/ctrl etc). If it is, send it to the
	  * editor for processing. If it is a shift event, then it may be a capital letter so we
	  * try to handle it.
	  */
	if (event->modifiers() != Qt::NoModifier && event->modifiers() != Qt::ShiftModifier && !shortcutPressed)
	{
		hide();

		editor->processKeyEvent(event);
		editor->setFocus();
	}

	// If popup visible and up/down keys pressed, send it to the subclass for going up/down list.
	else if (isVisible() && ((event->key() == Qt::Key_Down) || (event->key() == Qt::Key_Up)))
		QListWidget::keyPressEvent(event);

	// If popup visible and left key pressed, scroll one step to the left.
	else if (isVisible() && event->key() == Qt::Key_Left)
		horizontalScrollBar()->setValue(horizontalScrollBar()->value()
						- horizontalScrollBar()->singleStep());

	// If popup visible and right key pressed, scroll one step to the right.
	else if (isVisible() && event->key() == Qt::Key_Right)
		horizontalScrollBar()->setValue(horizontalScrollBar()->value()
						+ horizontalScrollBar()->singleStep());

	// If the popup isn't visible and a directional cursor pressed, we pass the event back to editor.
	else if (!isVisible() && ((event->key() == Qt::Key_Left) || (event->key() == Qt::Key_Right)
		|| (event->key() == Qt::Key_Up) || (event->key() == Qt::Key_Down)))
	{
		editor->processKeyEvent(event);
		editor->setFocus();
	}

	// If the popup is visible and enter/return pressed, we accept the current completion in the list.
	else if (isVisible() && ((event->key() == Qt::Key_Enter) || (event->key() == Qt::Key_Return)))
	{
		QTextCursor cursor = editor->textCursor();

		cursor.select(QTextCursor::WordUnderCursor);
		cursor.removeSelectedText();
		cursor.insertText(currentItem()->text());

		hide();

		editor->setFocus();
	}

	// If popup not visible and backspace pressed, pass the event back to editor.
	else if (!isVisible() && event->key() == Qt::Key_Backspace)
	{
		editor->processKeyEvent(event);
		editor->setFocus();
	}

	/*
	  * If popup visible and backspace pressed and the word to be completed is now
	  * smaller then the needed size, hide the popup and send event back to editor.
	  */
	else if (isVisible() && (event->key() == Qt::Key_Backspace)
		&& (editor->getWordToComplete().length() <= Preferences::getInstance()->getValue(Preferences::SETTING_AUTO_COMPLETION_PREFIX_LENGTH).toInt()))
	{
		hide();

		editor->processKeyEvent(event);
		editor->setFocus();
	}

	// If popup visible and space/tab pressed, we start a new word so hide completer.
	else if (isVisible() && ((event->key() == Qt::Key_Space) || (event->key() == Qt::Key_Tab)))
	{
		hide();

		editor->processKeyEvent(event);
		editor->setFocus();
	}

	// If popup visible and escape pressed, close the popup, but the event is ours.
	else if (isVisible() && event->key() == Qt::Key_Escape)
	{
		hide();

		editor->setFocus();
	}

	// Yay! the event has passed the test and we are ready to prepare a list of completions.
	else
	{
		int bestWidth = 0, bestHeight = 0;

		// Pass event to editor (so the character is typed.
		editor->processKeyEvent(event);

		// Fetch new list of completions in document.
		updateCompletions();

		// Take away completions that are not relevent.
		for (int i = 0; i < count(); i++)
		{
			if (!item(i)->text().startsWith(editor->getWordToComplete(), caseSensitivity))
				takeItem(i--);
		}

		// Get width of widest completion.
		for (int i = 0; i < count(); i++)
		{
			QModelIndex index = indexFromItem(item(i));
			QAbstractItemDelegate *delegate = itemDelegate();

			bestWidth = qMax(delegate->sizeHint(QStyleOptionViewItem(),
							    index).width(), bestWidth);
		}

		// Get height of a completion (assuming they are all one line, which they are in this case).
		if (count() > 0)
		{
			QModelIndex index = indexFromItem(item(0));
			QAbstractItemDelegate *delegate = itemDelegate();

			// Add a buffer so that a scrollbar can be shown.
			// Would be better if we could do a verticalScrollBar.width() here but
			// the very first time it will have a value of 100 until it is actually
			// shown before it will have a correct value, so that makes it buggy.
			// Don't know if there is a way to get the scrollbar width without actually
			// showing it first.
			bestWidth += 25;

			// Add a buffer to the height, this seems to be the best to give as little as
			// possible without inducing scroll bars to be shown.
			bestHeight = delegate->sizeHint(QStyleOptionViewItem(), index).height();
			bestHeight *= count();
			bestHeight += qMax(2, 2 * (count() - (count() - 1)));
		}

		// Careful here: for some reason the height/width of the scrollbar is invalid (like 100)
		// until it is actually shown, then the next time it reports the correct value. STUPID.
		// That said, this one doesn't seem to cause any bugs...
		if (bestWidth > MAX_WIDTH + 25)
			bestHeight += 25;
			//bestHeight += horizontalScrollBar()->height();

		// Resize popup to minimum values.
		resize(qMin(bestWidth, MAX_WIDTH), qMin(bestHeight, MAX_HEIGHT));

		sortItems();

		// Show popup in the correct place.
		movePopup();
	}
}

/**
  * Move completion popup. Show/hide popup accordingly.
  */
void AutoCompletion::movePopup()
{
	// Show popup in the correct place.
	if ((count() > 0) && ((editor->getWordToComplete().length() == Preferences::getInstance()->getValue(
			Preferences::SETTING_AUTO_COMPLETION_PREFIX_LENGTH).toInt())
		|| shortcutPressed || isVisible()))
	{
		QRect cursor = editor->cursorRect();
		bool enoughSpace = true;

		if (editor->getWordToComplete().length() == Preferences::getInstance()->getValue(
				Preferences::SETTING_AUTO_COMPLETION_PREFIX_LENGTH).toInt()
			|| shortcutPressed)
		{
			int posX = cursor.x() + cursor.width() + editor->mapToGlobal(editor->pos()).x();
			int posY = cursor.y() + cursor.height() + editor->mapToGlobal(editor->pos()).y();
			QRect desktop = qApp->desktop()->availableGeometry();

			// Can't place popup below cursor, try above.
			if (posY + height() > desktop.y() + desktop.height())
			{
				posY = cursor.y() + editor->mapToGlobal(editor->pos()).y() - height();

				// Check if we can place popup above cursor.
				if (desktop.y() > posY)
					enoughSpace = false;

				// Move popup to above cursor.
				else
					move(posX, cursor.y() + editor->mapToGlobal(editor->pos()).y() - height());
			}

			// Position popup below cursor.
			else
				move(posX, posY);
		}

		// There is enough space to show popup, show it.
		// If there is not enough space (above/below cursor), we just don't show it.
		if (enoughSpace)
		{
			show();
			setFocus();
			setCurrentRow(0);
		}
	}

	// Event causes completion popup to piss off.
	else
	{
		hide();

		editor->setFocus();
	}
}
